import GraphQL
import Testing

/// This follows the graphql-js testing, with deviations where noted.
@Suite struct SubscriptionTests {
    let timeoutDuration = 0.5 // in seconds

    // MARK: Test primary graphqlSubscribe function

    /// This test is not present in graphql-js, but just tests basic functionality.
    @Test func graphqlSubscribe() async throws {
        let db = EmailDb()
        let schema = try await db.defaultSchema()
        let query = """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """

        let subscriptionResult = try await GraphQL.graphqlSubscribe(
            schema: schema,
            request: query
        )
        let stream = try subscriptionResult.get()
        var iterator = stream.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))
        await db.stop()
        let result = try await iterator.next()
        #expect(
            result == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )
    }

    // MARK: Subscription Initialization Phase

    /// accepts multiple subscription fields defined in schema
    @Test func acceptsMultipleSubscriptionFields() async throws {
        let db = EmailDb()
        let schema = try GraphQLSchema(
            query: EmailQueryType,
            subscription: GraphQLObjectType(
                name: "Subscription",
                fields: [
                    "importantEmail": GraphQLField(
                        type: EmailEventType,
                        args: [
                            "priority": GraphQLArgument(
                                type: GraphQLInt
                            ),
                        ],
                        resolve: { emailAny, _, _, _ throws in
                            guard let email = emailAny as? Email else {
                                throw GraphQLError(
                                    message: "Source is not Email type: \(type(of: emailAny))"
                                )
                            }
                            return EmailEvent(
                                email: email,
                                inbox: Inbox(emails: await db.emails)
                            )
                        },
                        subscribe: { _, _, _, _ throws in
                            await db.publisher.subscribe()
                        }
                    ),
                    "notImportantEmail": GraphQLField(
                        type: EmailEventType,
                        args: [
                            "priority": GraphQLArgument(
                                type: GraphQLInt
                            ),
                        ],
                        resolve: { emailAny, _, _, _ throws in
                            guard let email = emailAny as? Email else {
                                throw GraphQLError(
                                    message: "Source is not Email type: \(type(of: emailAny))"
                                )
                            }
                            return EmailEvent(
                                email: email,
                                inbox: Inbox(emails: await db.emails)
                            )
                        },
                        subscribe: { _, _, _, _ throws in
                            await db.publisher.subscribe()
                        }
                    ),
                ]
            )
        )
        let stream = try await createSubscription(schema: schema, query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))

        let result = try await iterator.next()
        #expect(
            result == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )
    }

    /// 'should only resolve the first field of invalid multi-field'
    ///
    /// Note that due to implementation details in Swift, this will not resolve the "first" one,
    /// but rather a random one of the two
    @Test func invalidMultiField() async throws {
        let db = EmailDb()

        actor ResolveChecker {
            var didResolveImportantEmail = false
            var didResolveNonImportantEmail = false
            func resolveImportantEmail() async {
                didResolveImportantEmail = true
            }

            func resolveNonImportantEmail() async {
                didResolveNonImportantEmail = true
            }
        }
        let resolveChecker = ResolveChecker()

        let schema = try GraphQLSchema(
            query: EmailQueryType,
            subscription: GraphQLObjectType(
                name: "Subscription",
                fields: [
                    "importantEmail": GraphQLField(
                        type: EmailEventType,
                        resolve: { _, _, _, _ in
                            nil
                        },
                        subscribe: { _, _, _, _ in
                            await resolveChecker.resolveImportantEmail()
                            return await db.publisher.subscribe()
                        }
                    ),
                    "notImportantEmail": GraphQLField(
                        type: EmailEventType,
                        resolve: { _, _, _, _ in
                            nil
                        },
                        subscribe: { _, _, _, _ in
                            await resolveChecker.resolveNonImportantEmail()
                            return await db.publisher.subscribe()
                        }
                    ),
                ]
            )
        )
        let subscriptionResult = try await createSubscription(schema: schema, query: """
            subscription {
                importantEmail {
                    email {
                        from
                    }
                }
                notImportantEmail {
                    email {
                        from
                    }
                }
            }
        """)
        var iterator = subscriptionResult.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))

        _ = try await iterator.next()

        // One and only one should be true
        let didResolveImportantEmail = await resolveChecker.didResolveImportantEmail
        let didResolveNonImportantEmail = await resolveChecker.didResolveNonImportantEmail
        #expect(didResolveImportantEmail || didResolveNonImportantEmail)
        #expect(!(didResolveImportantEmail && didResolveNonImportantEmail))
    }

    // 'throws an error if schema is missing'
    // Not implemented because this is taken care of by Swift optional types

    // 'throws an error if document is missing'
    // Not implemented because this is taken care of by Swift optional types

    /// 'resolves to an error for unknown subscription field'
    @Test func errorUnknownSubscriptionField() async throws {
        let db = EmailDb()
        do {
            _ = try await db.subscription(query: """
            subscription {
                unknownField
            }
            """)
            Issue.record("Error should have been thrown")
        } catch {
            guard let graphQLErrors = error as? GraphQLErrors else {
                Issue.record("Error was not of type GraphQLErrors")
                return
            }
            #expect(
                graphQLErrors.errors == [
                    GraphQLError(
                        message: "Cannot query field \"unknownField\" on type \"Subscription\".",
                        locations: [SourceLocation(line: 2, column: 5)]
                    ),
                ]
            )
        }
    }

    /// 'should pass through unexpected errors thrown in subscribe'
    @Test func passUnexpectedSubscribeErrors() async throws {
        let db = EmailDb()
        do {
            _ = try await db.subscription(query: "")
            Issue.record("Error should have been thrown")
        } catch {}
    }

    /// 'throws an error if subscribe does not return an iterator'
    @Test func errorIfSubscribeIsntIterator() async throws {
        let schema = try emailSchemaWithResolvers(
            resolve: { _, _, _, _ throws in
                nil
            },
            subscribe: { _, _, _, _ throws in
                "test"
            }
        )
        do {
            _ = try await createSubscription(schema: schema, query: """
                subscription {
                    importantEmail {
                        email {
                            from
                        }
                    }
                }
            """)
            Issue.record("Error should have been thrown")
        } catch {
            guard let graphQLErrors = error as? GraphQLErrors else {
                Issue.record("Error was not of type GraphQLError")
                return
            }
            #expect(
                graphQLErrors.errors == [
                    GraphQLError(
                        message: "Subscription field resolver must return an AsyncSequence. Received: 'test'"
                    ),
                ]
            )
        }
    }

    /// 'resolves to an error for subscription resolver errors'
    @Test func errorForSubscriptionResolverErrors() async throws {
        func verifyError(schema: GraphQLSchema) async throws {
            do {
                _ = try await createSubscription(schema: schema, query: """
                    subscription {
                        importantEmail {
                            email {
                                from
                            }
                        }
                    }
                """)
                Issue.record("Error should have been thrown")
            } catch {
                guard let graphQLErrors = error as? GraphQLErrors else {
                    Issue.record("Error was not of type GraphQLError")
                    return
                }
                #expect(graphQLErrors.errors == [GraphQLError(message: "test error")])
            }
        }

        // Throwing an error
        try await verifyError(schema: emailSchemaWithResolvers(
            subscribe: { _, _, _, _ throws in
                throw GraphQLError(message: "test error")
            }
        ))

        // Resolving to an error
        try await verifyError(schema: emailSchemaWithResolvers(
            subscribe: { _, _, _, _ throws in
                GraphQLError(message: "test error")
            }
        ))

        // Rejecting with an error
        try await verifyError(schema: emailSchemaWithResolvers(
            subscribe: { _, _, _, _ throws in
                GraphQLError(message: "test error")
            }
        ))
    }

    /// 'resolves to an error for source event stream resolver errors'
    // Tests above cover this

    /// 'resolves to an error if variables were wrong type'
    @Test func errorVariablesWrongType() async throws {
        let db = EmailDb()
        let query = """
            subscription ($priority: Int) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """

        do {
            _ = try await db.subscription(
                query: query,
                variableValues: [
                    "priority": "meow",
                ]
            )
            Issue.record("Should have thrown error")
        } catch {
            guard let graphQLError = error as? GraphQLError else {
                Issue.record("Error was not of type GraphQLError")
                return
            }
            #expect(
                graphQLError.message ==
                    "Variable \"$priority\" got invalid value \"\"meow\"\".\nExpected type \"Int\", found \"meow\"."
            )
        }
    }

    // MARK: Subscription Publish Phase

    /// 'produces a payload for a single subscriber'
    @Test func singleSubscriber() async throws {
        let db = EmailDb()
        let stream = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))
        await db.stop()

        let result = try await iterator.next()
        #expect(
            result == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )
    }

    /// 'produces a payload for multiple subscribe in same subscription'
    @Test func multipleSubscribers() async throws {
        let db = EmailDb()
        let stream1 = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)

        let stream2 = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)

        var iterator1 = stream1.makeAsyncIterator()
        var iterator2 = stream2.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))

        let result1 = try await iterator1.next()
        let result2 = try await iterator2.next()

        let expected = GraphQLResult(
            data: ["importantEmail": [
                "email": [
                    "from": "yuzhi@graphql.org",
                    "subject": "Alright",
                ],
                "inbox": [
                    "unread": 1,
                    "total": 2,
                ],
            ]]
        )

        #expect(result1 == expected)
        #expect(result2 == expected)
    }

    /// 'produces a payload per subscription event'
    @Test func payloadPerEvent() async throws {
        let db = EmailDb()
        let stream = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()

        // A new email arrives!
        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))
        let result1 = try await iterator.next()
        #expect(
            result1 == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )

        // Another new email arrives
        await db.trigger(email: Email(
            from: "hyo@graphql.org",
            subject: "Tools",
            message: "I <3 making things",
            unread: true
        ))
        let result2 = try await iterator.next()
        #expect(
            result2 == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "hyo@graphql.org",
                        "subject": "Tools",
                    ],
                    "inbox": [
                        "unread": 2,
                        "total": 3,
                    ],
                ]]
            )
        )
    }

    /// Tests that subscriptions use arguments correctly.
    /// This is not in the graphql-js tests.
    @Test func arguments() async throws {
        let db = EmailDb()
        let stream = try await db.subscription(query: """
            subscription ($priority: Int = 5) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()
        var results = [GraphQLResult?]()
        var expected = [GraphQLResult]()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true,
            priority: 7
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)

        // Low priority email shouldn't trigger an event
        await db.trigger(email: Email(
            from: "hyo@graphql.org",
            subject: "Not Important",
            message: "Ignore this email",
            unread: true,
            priority: 2
        ))
        #expect(results == expected)

        // Higher priority one should trigger again
        await db.trigger(email: Email(
            from: "hyo@graphql.org",
            subject: "Tools",
            message: "I <3 making things",
            unread: true,
            priority: 5
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "hyo@graphql.org",
                        "subject": "Tools",
                    ],
                    "inbox": [
                        "unread": 3,
                        "total": 4,
                    ],
                ]]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)
    }

    /// 'should not trigger when subscription is already done'
    @Test func noTriggerAfterDone() async throws {
        let db = EmailDb()
        let stream = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                  inbox {
                    unread
                    total
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()
        var results = [GraphQLResult?]()
        var expected = [GraphQLResult]()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                    "inbox": [
                        "unread": 1,
                        "total": 2,
                    ],
                ]]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)

        await db.stop()

        // This should not trigger an event.
        await db.trigger(email: Email(
            from: "hyo@graphql.org",
            subject: "Tools",
            message: "I <3 making things",
            unread: true
        ))
        // Ensure that the current result was the one before the db was stopped
        #expect(results == expected)
    }

    /// 'should not trigger when subscription is thrown'
    // Not necessary - Swift async stream handles throwing errors

    /// 'event order is correct for multiple publishes'
    @Test func orderCorrectForMultiplePublishes() async throws {
        let db = EmailDb()
        let stream = try await db.subscription(query: """
            subscription ($priority: Int = 0) {
                importantEmail(priority: $priority) {
                  email {
                    from
                    subject
                  }
                }
              }
        """)
        var iterator = stream.makeAsyncIterator()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Alright",
            message: "Tests are good",
            unread: true
        ))
        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Message 2",
            message: "Tests are good 2",
            unread: true
        ))

        let result1 = try await iterator.next()
        let result2 = try await iterator.next()

        #expect(
            result1 == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Alright",
                    ],
                ]]
            )
        )
        #expect(
            result2 == GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "from": "yuzhi@graphql.org",
                        "subject": "Message 2",
                    ],
                ]]
            )
        )
    }

    /// 'should handle error during execution of source event'
    @Test func errorDuringSubscription() async throws {
        let db = EmailDb()

        let schema = try emailSchemaWithResolvers(
            resolve: { emailAny, _, _, _ throws in
                guard let email = emailAny as? Email else {
                    throw GraphQLError(
                        message: "Source is not Email type: \(type(of: emailAny))"
                    )
                }
                if email.subject == "Goodbye" { // Force the system to fail here.
                    throw GraphQLError(message: "Never leave.")
                }
                return EmailEvent(
                    email: email,
                    inbox: Inbox(emails: await db.emails)
                )
            },
            subscribe: { _, _, _, _ throws in
                await db.publisher.subscribe()
            }
        )

        let stream = try await createSubscription(schema: schema, query: """
            subscription {
                importantEmail {
                    email {
                        subject
                    }
                }
            }
        """)
        var iterator = stream.makeAsyncIterator()
        var results = [GraphQLResult?]()
        var expected = [GraphQLResult]()

        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Hello",
            message: "Tests are good",
            unread: true
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "subject": "Hello",
                    ],
                ]]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)

        // An error in execution is presented as such.
        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Goodbye",
            message: "Tests are good",
            unread: true
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": nil],
                errors: [
                    GraphQLError(message: "Never leave."),
                ]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)

        // However that does not close the response event stream. Subsequent events are still
        // executed.
        await db.trigger(email: Email(
            from: "yuzhi@graphql.org",
            subject: "Bonjour",
            message: "Tests are good",
            unread: true
        ))
        expected.append(
            GraphQLResult(
                data: ["importantEmail": [
                    "email": [
                        "subject": "Bonjour",
                    ],
                ]]
            )
        )
        try await results.append(iterator.next())
        #expect(results == expected)
    }

    /// 'should pass through error thrown in source event stream'
    // Handled by AsyncThrowingStream

    /// Test incorrect emitted type errors
    // Handled by strongly typed PubSub
}
